/**
 * $version:  0.1 
 * $Date: 2011-01-21 
 *
 * Copyright (C) 2010-2011 Jinrui Software. All rights reserved.
 *
 */

package org.jawa.core.util;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.commons.lang.StringEscapeUtils;
import org.dom4j.CDATA;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 提供对简单的XML文件的读写。 
 * <p>
 * 类似 X.Y.Z 属性在XML中定义如下：
 * </p>
 * 
 * <pre>
 * &lt;X&gt;
 *     &lt;Y&gt;
 *         &lt;Z&gt;someValue&lt;/Z&gt;
 *     &lt;/Y&gt;
 * &lt;/X&gt;
 * </pre>
 * <p/>
 * XML 文件通过构造文件传入，该文件必须是可读和可写的。当设置属性值时，会自动保存到XML文件中。该XML文件使用的编码是 UTF-8。 The XML
 * 
 * @author Y-N
 */
public class XMLProperties {

	private static final Logger Log = LoggerFactory.getLogger(XMLProperties.class);

	private File file;
	private Document document;

	/**
	 * 每次访问XML文件太慢，所以使用一个Map缓存被访问一次以上的属性值。
	 */
	private Map<String, String> propertyCache = new HashMap<String, String>();

	/**
	 * 通过一个文件名称来创建一个新的XMLProperties对象。
	 * 
	 * @param fileName
	 *            文件名称
	 * @throws IOException
	 *             加载文件时发生错误
	 */
	public XMLProperties(String fileName) throws IOException {
		this(new File(fileName));
	}

	/**
	 * 通过输入流对象来创建一个XMLProperties对象。
	 * 
	 * @param in
	 *            XML 文件的输入流
	 * @throws IOException
	 *             加载文件时发生错误
	 */
	public XMLProperties(InputStream in) throws IOException {
		Reader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
		buildDoc(reader);
	}

	/**
	 * 通过文件来创建一个XMLProperties对象。
	 * 
	 * @param file
	 *            文件
	 * @throws IOException
	 *             加载文件时发生错误
	 */
	public XMLProperties(File file) throws IOException {
		this.file = file;
		if (!file.exists()) {
			throw new FileNotFoundException("XML 属性文件不存在: " + file.getName());
		}
		// 检查读写权限
		if (!file.canRead()) {
			throw new IOException("XML 属性文件不可读: " + file.getName());
		}
		if (!file.canWrite()) {
			throw new IOException("XML 属性文件不可写 : " + file.getName());
		}

		Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
		buildDoc(reader);
	}

	/**
	 * 返回配制文件中的一个属性。
	 * 
	 * @param name
	 *            元素名称，格式为 X.Y.Z
	 * @return 元素内容
	 */
	public synchronized String getProperty(String name) {
		String value = propertyCache.get(name);
		if (value != null) {
			return value;
		}

		String[] propName = parsePropertyName(name);
		Element element = document.getRootElement();
		for (String aPropName : propName) {
			element = element.element(aPropName);
			if (element == null) {
				return null;
			}
		}
		value = element.getTextTrim();
		if ("".equals(value)) {
			return null;
		} else {
			propertyCache.put(name, value);
			return value;
		}
	}

	/**
	 * 
	 * 返回指定元素名称的所有的元素值列表。
	 * <p>
	 * 元素在 XML 文件中定义如下所示:
	 * </p>
	 * 
	 * <pre>
	 * &lt;foo&gt;
	 *     &lt;bar&gt;
	 *         &lt;prop&gt;some value&lt;/prop&gt;
	 *         &lt;prop&gt;other value&lt;/prop&gt;
	 *         &lt;prop&gt;last value&lt;/prop&gt;
	 *     &lt;/bar&gt;
	 * &lt;/foo&gt;
	 * </pre>
	 * 
	 * 通过getProperties("foo.bar.prop")方法将返回{"some value",
	 * "other value","last value"}。
	 * 
	 * 
	 * @param name
	 *            元素名称
	 * @return 所有元素值
	 */
	@SuppressWarnings("unchecked")
	public String[] getProperties(String name) {
		String[] propName = parsePropertyName(name);
		Element element = document.getRootElement();
		for (int i = 0; i < propName.length - 1; i++) {
			element = element.element(propName[i]);
			if (element == null) {
				return new String[] {};
			}
		}
		Iterator<Element> iter = element.elementIterator(propName[propName.length - 1]);
		List<String> props = new ArrayList<String>();
		String value;
		while (iter.hasNext()) {
			value = ((Element) iter.next()).getTextTrim();
			if (!"".equals(value)) {
				props.add(value);
			}
		}
		String[] childrenNames = new String[props.size()];
		return props.toArray(childrenNames);

	}

	/**
	 * 根据给定的路径的元素名称获取元素值列表。
	 * <pre>
	 * &lt;foo&gt;
	 *     &lt;bar&gt;
	 *         &lt;prop&gt;some value&lt;/prop&gt;
	 *         &lt;prop&gt;other value&lt;/prop&gt;
	 *         &lt;prop&gt;last value&lt;/prop&gt;
	 *     &lt;/bar&gt;
	 * &lt;/foo&gt;
	 * </pre>
	 * 例：调用 getProperties("foo.bar.prop") 将返回如下的数组对象
	 * {"some value", "other value", "last value"}.
	 *
	 * @param name 需要检索的属性值
	 * @return 返回所有指定路径下的子节点
	 */
	@SuppressWarnings("rawtypes")
	public Iterator getChildProperties(String name) {
		String[] propName = parsePropertyName(name);
		// Search for this property by traversing down the XML heirarchy,
		// stopping one short.
		Element element = document.getRootElement();
		for (int i = 0; i < propName.length - 1; i++) {
			element = element.element(propName[i]);
			if (element == null) {
				// This node doesn't match this part of the property name which
				// indicates this property doesn't exist so return empty array.
				return Collections.EMPTY_LIST.iterator();
			}
		}
		// We found matching property, return values of the children.
		Iterator iter = element.elementIterator(propName[propName.length - 1]);
		ArrayList<String> props = new ArrayList<String>();
		while (iter.hasNext()) {
			props.add(((Element) iter.next()).getText());
		}
		return props.iterator();
	}

	/**
	 * 返回元素名称下所有的子元素名称。
	 * <p>
	 * 元素在 XML 文件中定义如下所示:
	 * </p>
	 * 
	 * <pre>
	 * &lt;database&gt;
	 * &lt;defaultProvider&gt;
	 * 	&lt;driver&gt;oracle.jdbc.driver.OracleDriver&lt;/driver&gt;
	 * 	&lt;serverURL&gt;jdbc:oracle:thin:@0.0.0.0:1521:ora10g&lt;/serverURL&gt;
	 * 	&lt;username&gt;user&lt;/username&gt;
	 * 	&lt;password&gt;password&lt;/password&gt;
	 * 	&lt;minConnections&gt;7&lt;/minConnections&gt;
	 * 	&lt;maxConnections&gt;10&lt;/maxConnections&gt;
	 * &lt;/defaultProvider&gt;
	 * &lt;/database&gt;
	 * </pre>
	 * 
	 * 通过getChildrenProperties("database.defaultProvider")方法将返回{driver,serverURL
	 * ,username,password,minConnections,maxConnections,maxConnections,
	 * maxConnections,maxConnections,maxConnections}。
	 * 
	 * @param parent
	 *            父元素名称
	 * @return 子元素名称
	 */
	@SuppressWarnings("rawtypes")
	public String[] getChildrenPropertieNames(String parent) {
		String[] propName = parsePropertyName(parent);
		Element element = document.getRootElement();
		for (String aPropName : propName) {
			element = element.element(aPropName);
			if (element == null) {
				return new String[] {};
			}
		}
		List children = element.elements();
		int childCount = children.size();
		String[] childrenNames = new String[childCount];
		for (int i = 0; i < childCount; i++) {
			childrenNames[i] = ((Element) children.get(i)).getName();
		}
		return childrenNames;
	}

	/**
	 * 返回指定元素的某个属性值。如果不存在则返加一个<tt>null</tt> 。
	 * 
	 * <p>
	 * 属性在 XML 文件中定义如下所示:
	 * </p>
	 * 
	 * <pre>
	 * &lt;foo&gt;
	 *     &lt;bar id="100" &gt;
	 *         &lt;prop&gt;some value&lt;/prop&gt;
	 *         &lt;prop&gt;other value&lt;/prop&gt;
	 *         &lt;prop&gt;last value&lt;/prop&gt;
	 *     &lt;/bar&gt;
	 * &lt;/foo&gt;
	 * </pre>
	 * 
	 * 通过getProperties("foo.bar","id")方法将返回100。
	 * 
	 * @param name
	 *            元素名称 如 "foo.bar"
	 * @param attribute
	 *            元素的某个属性 如 "id"
	 * @return 元素的某个属性值
	 */
	public String getAttribute(String name, String attribute) {
		if (name == null || attribute == null) {
			return null;
		}
		String[] propName = parsePropertyName(name);
		Element element = document.getRootElement();
		for (String child : propName) {
			element = element.element(child);
			if (element == null) {
				break;
			}
		}
		if (element != null) {
			return element.attributeValue(attribute);
		}
		return null;
	}

	/**
	 * 给一个元素加入多个元素值。
	 * <p>
	 * 例如，给元素 "foo.bar.prop"设置如下元素值 {"some value", "other value",
	 * "last value"}将生成如下的XML：
	 * </p>
	 * 
	 * <pre>
	 * &lt;foo&gt;
	 *     &lt;bar&gt;
	 *         &lt;prop&gt;some value&lt;/prop&gt;
	 *         &lt;prop&gt;other value&lt;/prop&gt;
	 *         &lt;prop&gt;last value&lt;/prop&gt;
	 *     &lt;/bar&gt;
	 * &lt;/foo&gt;
	 * </pre>
	 * 
	 * @param name
	 *            元素名称
	 * @param values
	 *            元素值
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void setProperties(String name, List<String> values) {
		String[] propName = parsePropertyName(name);
		Element element = document.getRootElement();
		for (int i = 0; i < propName.length - 1; i++) {
			if (element.element(propName[i]) == null) {
				element.addElement(propName[i]);
			}
			element = element.element(propName[i]);
		}
		String childName = propName[propName.length - 1];
		List<Element> toRemove = new ArrayList<Element>();
		Iterator<Element> iter = element.elementIterator(childName);
		while (iter.hasNext()) {
			toRemove.add((Element) iter.next());
		}
		for (iter = toRemove.iterator(); iter.hasNext();) {
			element.remove((Element) iter.next());
		}
		for (String value : values) {
			Element childElement = element.addElement(childName);
			if (value.startsWith("<![CDATA[")) {
				Iterator it = childElement.nodeIterator();
				while (it.hasNext()) {
					Node node = (Node) it.next();
					if (node instanceof CDATA) {
						childElement.remove(node);
						break;
					}
				}
				childElement.addCDATA(value.substring(9, value.length() - 3));
			} else {
				childElement.setText(StringEscapeUtils.escapeXml(value));
			}
		}
		saveProperties();

	}

	/**
	 * 给一个元素加入元素值。
	 * 
	 * @param name
	 *            元素名称
	 * @param value
	 *            元素值
	 */
	@SuppressWarnings("rawtypes")
	public synchronized void setProperty(String name, String value) {
		if (!StringEscapeUtils.escapeXml(name).equals(name)) {
			throw new IllegalArgumentException("Property name cannot contain XML entities.");
		}
		if (name == null) {
			return;
		}
		if (value == null) {
			value = "";
		}

		propertyCache.put(name, value);

		String[] propName = parsePropertyName(name);
		Element element = document.getRootElement();
		for (String aPropName : propName) {
			if (element.element(aPropName) == null) {
				element.addElement(aPropName);
			}
			element = element.element(aPropName);
		}
		if (value.startsWith("<![CDATA[")) {
			Iterator it = element.nodeIterator();
			while (it.hasNext()) {
				Node node = (Node) it.next();
				if (node instanceof CDATA) {
					element.remove(node);
					break;
				}
			}
			element.addCDATA(value.substring(9, value.length() - 3));
		} else {
			element.setText(value);
		}
		saveProperties();
	}

	/**
	 * 删除元素。
	 * 
	 * @param name
	 *            元素名称
	 */
	public synchronized void deleteProperty(String name) {
		propertyCache.remove(name);

		String[] propName = parsePropertyName(name);
		Element element = document.getRootElement();
		for (int i = 0; i < propName.length - 1; i++) {
			element = element.element(propName[i]);
			if (element == null) {
				return;
			}
		}
		element.remove(element.element(propName[propName.length - 1]));
		saveProperties();

	}

	/**
	 * 生成 XML model
	 * 
	 * @param in
	 *            xml 文档流
	 * @throws java.io.IOException
	 *             读取文件发生异常
	 */
	private void buildDoc(Reader in) throws IOException {
		try {
			SAXReader xmlReader = new SAXReader();
			xmlReader.setEncoding("UTF-8");
			document = xmlReader.read(in);
		} catch (Exception e) {
			Log.error("Error reading XML properties", e);
			throw new IOException(e.getMessage());
		} finally {
			if (in != null) {
				in.close();
			}
		}
	}

	/**
	 * 将配置写入磁盘中。
	 * <p>
	 * 通过写入临时文件，保证最大的安全性。
	 * </p>
	 */
	private synchronized void saveProperties() {
		boolean error = false;
		File tempFile = null;
		Writer writer = null;
		try {
			tempFile = new File(file.getParentFile(), file.getName() + ".tmp");
			writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tempFile), "UTF-8"));
			OutputFormat prettyPrinter = OutputFormat.createPrettyPrint();
			prettyPrinter.setEncoding("UTF-8");
			XMLWriter xmlWriter = new XMLWriter(writer, prettyPrinter);
			xmlWriter.write(document);
		} catch (Exception e) {
			Log.error(e.getMessage(), e);
			error = true;
		} finally {
			if (writer != null) {
				try {
					writer.close();
				} catch (IOException e1) {
					Log.error(e1.getMessage(), e1);
					error = true;
				}
			}
		}

		if (!error) {
			if (!file.delete()) {
				Log.error("Error deleting property file: " + file.getAbsolutePath());
				return;
			}
			try {
				copy(tempFile, file);
			} catch (Exception e) {
				Log.error(e.getMessage(), e);
				error = true;
			}
			if (!error) {
				tempFile.delete();
			}
		}
	}

	/**
	* 根据给定的元素返回字符型数组。
	* 元素格式类似 "prop.name.is.this" 
	*
	* @param name 指定的元素名
	* @return 根据给定的元素返回字符型数组
	*/
	private String[] parsePropertyName(String name) {
		List<String> propName = new ArrayList<String>(5);
		StringTokenizer tokenizer = new StringTokenizer(name, ".");
		while (tokenizer.hasMoreTokens()) {
			propName.add(tokenizer.nextToken());
		}
		return propName.toArray(new String[propName.size()]);
	}

	public void setProperties(Map<String, String> propertyMap) {
		for (String propertyName : propertyMap.keySet()) {
			String propertyValue = propertyMap.get(propertyName);
			setProperty(propertyName, propertyValue);
		}
	}

	/**
	 * 文件复制。
	 * 
	 * @param inFile
	 *            要复制的文件
	 * @param outFile
	 *            复制文件的副本
	 * @throws IOException
	 *             复制文件过程中发生读写错误
	 */
	private static void copy(File inFile, File outFile) throws IOException {
		FileInputStream fin = null;
		FileOutputStream fout = null;
		try {
			fin = new FileInputStream(inFile);
			fout = new FileOutputStream(outFile);
			copy(fin, fout);
		} finally {
			try {
				if (fin != null)
					fin.close();
			} catch (IOException e) {
				// 忽略
			}
			try {
				if (fout != null)
					fout.close();
			} catch (IOException e) {
				// 忽略
			}
		}
	}

	/**
	 * 从输入流中拷贝数据到输出流
	 * 
	 * @param in
	 *            输入流
	 * @param out
	 *            输出流
	 * @throws IOException
	 *             在复制过程中发生异常
	 */
	private static void copy(InputStream in, OutputStream out) throws IOException {
		synchronized (in) {
			synchronized (out) {
				byte[] buffer = new byte[256];
				while (true) {
					int bytesRead = in.read(buffer);
					if (bytesRead == -1)
						break;
					out.write(buffer, 0, bytesRead);
				}
			}
		}
	}
}
